/*
*
*	The Gimme ECMAScript Library by Stephen Stchur
*	"Taking the pain out of Javascript!"
*
*	Copyright (c) Microsoft Corporation.  All rights reserved.
*
*	Description:
*	Gimme Object Module:	Wrapper around Gimme Query
*
*	Requires:
*	Gimme.query.js
*
*/

// Wrap a new Gimme.object(..) invocation in the function g(..)
function g(selector)
{
	return new Gimme.object(Gimme.query(selector));
}

Gimme.object = function(elems)
{
	this.entities = elems;
	this.length = this.entities.length;
};
Gimme.ext = Gimme.object.prototype;

(function()
{
	var h = Gimme.Helper
	,contains = h.contains
	,indexOf = h.indexOf
	,trim = h.trim
	,normalize = h.normalize
	,hasClass = h.hasClass
	,attrExists = h.attrExists
	,readAttr = h.readAttribute
	,getObjectGUID = h.getObjectGUID
	,convertToPixels = h.convertToPixels;

	Gimme.ext.element = function(n)
	{
		return this.entities[n || 0];
	};

	Gimme.ext.parent = function(n)
	{
		return this.entities[n || 0].parentNode;
	};

	Gimme.ext.addClass = function(_class)
	{
		var classes = _class.split(/\s+/);
		
		this.forEach(function(_elem)
		{
			g(classes).forEach(function(_className)
			{
				if (!hasClass(_elem, [ _className ]))
				{
					if (_elem.className === '')
					{
						_elem.className = _className;
					}
					else
					{
						_elem.className += ' ' + _className;
					}
				}
			});
		});
		
		return this;
	};

	Gimme.ext.removeClass = function(_class)
	{
		return this.swapClass(_class, '$1');
	};

	Gimme.ext.swapClass = function(_oldClass, _newClass)
	{
		// if _newClass is '$1' it means we're actually just removing _oldClass (and not replacing it with anything)
		// however, if _newClass is anything other than $1, we need to pad it with spaces 
		if (_newClass !== '$1')	{ _newClass = ' ' + _newClass + ' '; }
		
		var classes = _oldClass.split(/\s+/);
		
		this.forEach(function(_elem)
		{
			var tmpClass = _elem.className;
			
			g(classes).forEach(function(_className)
			{
				var pattern = new RegExp('(^| )' + _className + '( |$)');
				tmpClass = tmpClass.replace(pattern, _newClass);
			});
			
			_elem.className = normalize(tmpClass);
		});
		
		return this;
	};

	Gimme.ext.hasClass = function(_class, n)
	{
		return hasClass(this.entities[n || 0], [ _class ]);
	};

	Gimme.ext.getAncestor = function(_distance, n)
	{
		var elem = this.entities[n || 0];
		var i = _distance;
		while (i-- > 0)
		{
			if (elem)
			{
				elem = elem.parentNode;
			}
			else
			{
				break;
			}			
		}
		
		return elem;
	};

	Gimme.ext.getSibling = function(_distance, n)
	{
		var elem = this.entities[n || 0];
		
		if (_distance === 0)
		{
			return elem;
		}
		
		var sibling = _distance > 0 ? 'nextSibling' : 'previousSibling';

		var sib = elem;
		var count = Math.abs(_distance);

		var i = 0;
		while (i < count)
		{
			sib = sib[sibling];
			if (!sib)
			{
				break;
			}
			if (sib.nodeType === 1)
			{
				i++;
			}
		}

		return sib;
	};

	Gimme.ext.select = function(_selector)
	{
		return new Gimme.object(Gimme.query(_selector, this.entities[0]));
	};

	Gimme.ext.setHTML = function(_html)
	{
		this.forEach(function(_elem)
		{
			_elem.innerHTML = _html;
		});

		return this;
	};

	Gimme.ext.getHTML = function(n)
	{
		return this.entities[n || 0].innerHTML;
	};

	Gimme.ext.setValue = function(_val)
	{
		this.forEach(function(_elem)
		{
			if (typeof _elem.value !== 'undefined')
			{
				_elem.value = _val;
			}
		});
			
		return this;
	};

	Gimme.ext.getValue = function(n)
	{
		return this.entities[n || 0].value || '';
	};

	Gimme.ext.readAttribute = function(_attr, n)
	{
		return readAttr(this.entities[n || 0], _attr);
	};

	Gimme.ext.writeAttribute = function(_attr, _val)
	{
		this.forEach(function(_elem)
		{
			_elem.setAttribute(_attr, _val);
		});
		
		return this;
	};

	Gimme.ext.filter = function(_filterFn)
	{
		var entities = this.entities;
		var i = 0, len = entities.length;
		while (i < len)
		{
			if (!_filterFn(entities[i]))
			{
				entities.splice(i, 1);
				len--;
			}
			else
			{
				i++;
			}
		}
		
		return this;
	};

	Gimme.ext.iterate = function(_callback)
	{
		var entities = this.entities;
		var i, len = entities.length;
		for (i = 0; i < len; i++)
		{
			var gElem = g(entities[i]);
			_callback.call(gElem, i);
		}
		
		_callback = null;
		
		return this;
	};

	Gimme.ext.getStyle = function(_style, n, _smart)
	{
		var elem = this.entities[n || 0];
		
		// TODO:  Document this rather confusing piece of code and why it's necessary
		if (_style === 'opacity')
		{
			var opacity = getOpacity(elem);
			if (isNaN(opacity))
			{
				_style = opacity;
			}
			else
			{
				return opacity;
			}
		}
		
		// _smart, which default to true, means that the function will attempt to calculate
		// the 'bottom' or 'right' styles based on the 'top' or 'left' styles (if it needs to)
		if (_smart !== false) { _smart = true; }

		var computedStyle = '';						// will be set to a computedStyle object in W3C browsers, and a currentStyle object in IE
		
		// if the browser supports .defaultView.getComputedStyle, life is easy -- just use that
		if (typeof document.defaultView !== 'undefined' && typeof document.defaultView.getComputedStyle !== 'undefined')
		{
			computedStyle = document.defaultView.getComputedStyle(elem, null);
		}
		// otherwise, crap, we've got work to do
		else if (typeof elem.currentStyle !== 'undefined')
		{	
			// start by retrieving the element's .currentStyle property (this won't be normalized to pixels)
			computedStyle = elem.currentStyle;
			
			// If the style being requested is either of the positions: top or left, we'll use .offsetTop and .offsetLeft
			// * WARNING:
			// * Browsers have sharp disagreements about what .offsetTop and .offsetLeft mean, but it just so "happens"
			// * that in IE .offsetTop and .offsetLeft are equal to the computed top and the computed left. Firefox usually
			// * agrees with IE on this, but Konqueror, Opera, and Safari all disagree (with IE and FF).
			// * The major flaw in this function's logic is that it assumes .offsetTop and .offsetLeft are going to be the same
			// * as the computed top and computed left if the browser supports .currentStyle (and not .defaultView.getComputedStyle).
			// * At present, this appears to be true, and I suspect that it will always be true, but there is no guarantee.				
			
			// attempt to get the requested style
			var val = computedStyle[_style];
			
			// if the value is 'auto', it most likely means that it was never specified in the stylesheet
			if (val === 'auto')
			{
				// if height was requested, try .clientHeight; if that fails, try .offsetHeight
				if (_style === 'height')
				{
					var padding = parseInt(g(elem).getStyle('paddingTop')) + parseInt(g(elem).getStyle('paddingBottom'));
					if (elem.clientHeight)
					{
						return (elem.clientHeight - padding) + 'px';
					}					
					else
					{
						return (elem.offsetHeight - padding - parseInt(g(elem).getStyle('borderTopWidth')) - parseInt(g(elem).getStyle('borderBottomWidth'))) + 'px';
					}					
				}
				
				// if width was requested, try .clientWidth; if that fails, try .offsetWidth
				if (_style === 'width')
				{
					var padding = parseInt(g(elem).getStyle('paddingLeft')) + parseInt(g(elem).getStyle('paddingRight'));
					if (elem.clientWidth)
					{
						return (elem.clientWidth - padding) + 'px';
					}
					else
					{
						return (elem.offsetWidth - padding - parseInt(g(elem).getStyle('borderLeftWIdth')) - parseInt(g(elem).getStyle('borderRightWidth'))) + 'px';
					}
				}
				
				// if top was requested, use .offsetTop
				if (_style === 'top')
				{
					return elem.offsetTop + 'px';
				}
				// if left was requested, use .offsetLeft
				else if (_style === 'left')
				{
					return elem.offsetLeft + 'px';
				}
				// If bottom or right were requested, AND if _smart (true by default) was specified, then
				// try to calculate right based on left or bottom based on top
				else if (_smart && (_style === 'right' || _style === 'bottom'))
				{
					// flip serves as an easy was to request the compliment property
					var flip = { bottom: [ 'top', 'Height' ], right: [ 'left', 'Width' ] };
					
					// get the computedStyle of the compliment property (make sure _smart is false so we don't recurse infinitely)
					var compliment = parseInt(this.getComputedStyle(elem, flip[_style][0], false), 10);
					
					// return the calculated bottom or right based on the known top or left
					return (elem.parentNode['client' + flip[_style][1]] - elem['offset' + flip[_style][1]] - compliment) + 'px';
				}
				// if val is 'auto', but none of the above apply, assume 0px (this might not be safe -- need to look into this more)					
				else
				{
					return '0px';
				}
			}
			
			// Some values should not be converted to pixels (like hex color values for instance), so this regex aids in
			// determining when to try converting to pixels (specifically, only for valid units that are not pixels)
			var nonPixels = /(em|ex|%|in|cm|mm|pt|pc|small|medium|large|thin|thick)$/;
			
			// Based on the 
			var borderWidth = /border(.*)Width/i;
			var borderStyle = borderWidth.test(_style) ? _style.replace(borderWidth, 'border$1Style') : null;				
			
			// only bother attempting to convert valid units that are not already pixels
			if (nonPixels.test(val))
			{					
				// IE does something interesting.  If you ask for border[Top|Right|Bottom|Left]Width, IE will give it to you... NO MATTER WHAT!
				// Depending on who you ask, this may or may not make sense.  But it doesn't matter, it won't work for this function.
				// If, for example, the borderTopWidth is 5px, but the borderTopStyle is 'none', then we need to report borderTopWidth as 0!
				if (borderStyle !== null && computedStyle[borderStyle] === 'none')
					{ return '0px'; }
				else
					{ return h.convertToPixels(val, elem); }
			}
		}
		return computedStyle && computedStyle[_style];
	};

	Gimme.ext.setStyle = function(_style, _val)
	{
		return g(this.entities).setStyles(_style, _val);		
	};
	
	Gimme.ext.setStyles = function(/* variables number of arguments accepts*/)
	{
		var styleName
		,styleVal
		,args = arguments
		,len = args.length
		,i;
		
		if (len % 2 !== 0)
		{
			return;
		}
		
		this.forEach(function(_elem)
		{
			for (i = 0; i < len; i += 2)
			{
				styleName = args[i];
				styleVal = args[i + 1];
				if (styleName === 'opacity')
				{
					setOpacity(_elem, styleVal);
				}
				else
				{
					_elem.style[styleName] = styleVal;
				}
			}
		});
		
		return this;
	};
	
	Gimme.ext.addEvent = function()
	{
		if (typeof document.addEventListener !== 'undefined')
		{
			return w3c_addEvent;
		}
		else if (typeof document.attachEvent !== 'undefined')
		{
			return ie_addEvent;
		}
		else
		{
			// no modern event support I guess :-(
			return function() { };
		}
		
		function w3c_addEvent(_evtName, _fn, _useCapture, _directCall)
		{
			var eventFn = this['on' + _evtName];
						
			if (typeof eventFn === 'function' && _directCall !== false)
			{
				eventFn.call(this, _fn, _useCapture, true);
			}
			else
			{
				this.forEach(function(_elem)			
				{
					_elem.addEventListener(_evtName, _fn, _useCapture);
				});
			}
			
			return this;
		}
		
		function ie_addEvent(_evtName, _fn, _useCapture, _directCall)
		{
			var eventFn = this['on' + _evtName];
						
			if (typeof eventFn === 'function' && _directCall !== false)
			{
				eventFn.call(this, _fn, _useCapture, true);
			}
			else
			{
				this.forEach(function(_elem)
				{
					// create a key to identify this element/event/function combination
					var key = '{' + getObjectGUID(_elem) + '/' + _evtName + '/' + getObjectGUID(_fn) + '}';
					
					// if this element/event/combo has already been wired up, just return
					var f = evtHash[key];
					if (typeof f !== 'undefined')
						{ return; }
					
					// create a helper function to fix IE's lack of standards support
					f = function(e)
					{
						// map .target to .srcElement
						e.target = e.srcElement;
						
						// map .relatedTarget to either .toElement or .fromElement
						if (_evtName == 'mouseover') { e.relatedTarget = e.fromElement; }
						else if (_evtName == 'mouseout') { e.relatedTarget = e.toElement; }
							
						e.preventDefault = function() { e.returnValue = false; };
						e.stopPropagation = function() { e.cancelBubble = true; };
		
						// call the actual function, using entity (the element) as the 'this' object
						_fn.call(_elem, e);
						
						// null out these properties to prevent memory leaks
						e.target = null;
						e.relatedTarget = null;
						e.preventDefault = null;
						e.stopPropagation = null;
						e = null;
					};
					
					// add the helper function to the event hash
					evtHash[key] = f;

					// hook up the event (IE style)
					_elem.attachEvent('on' + _evtName, f);
					
					key = null;
					f = null;
				});
			}
			
			return this;
		}
	}();
	
	Gimme.ext.removeEvent = function()
	{
		if (typeof document.removeEventListener !== 'undefined')
		{
			return w3c_removeEvent;
		}
		else if (typeof document.detachEvent !== 'undefined')
		{
			return ie_removeEvent;
		}
		else
		{
			// no modern event support I guess :-(
			return function() { };
		}
		
		function w3c_removeEvent(_evtName, _fn, _useCapture, _directCall)
		{
			var eventFn = this['on' + _evtName];
						
			if (typeof eventFn === 'function' && _directCall !== false)
			{
				eventFn.call(this, _fn, _useCapture, false);
			}
			else
			{
				this.forEach(function(_elem)
				{
					_elem.removeEventListener(_evtName, _fn, _useCapture);
				});
			}
			
			return this;
		}
		
		function ie_removeEvent(_evtName, _fn, _useCapture, _directCall)
		{
			var eventFn = this['on' + _evtName];
						
			if (typeof eventFn === 'function' && _directCall !== false)
			{
				eventFn.call(this, _fn, _useCapture, false);
			}
			else
			{
				this.forEach(function(_elem)
				{
					// grab the event handler function from the hash based on its key
					var key = '{' + getObjectGUID(_elem) + '/' + _evtName + '/' + getObjectGUID(_fn) + '}';
					var f = evtHash[key];
					if (typeof f !== 'undefined')
					{
						// unhook the event and delete the element from the event hash
						_elem.detachEvent('on' + _evtName, f);
						delete evtHash[key];
					}

					// prevent IE memory leaks
					key = null;
					f = null;
				});
			}
			
			return this;
		}
	}();
	
	Gimme.ext.forEach = function()
	{
		return typeof Array.prototype.forEach !== 'undefined' ? native_forEach : custom_forEach;
		
		function native_forEach(_fn, _thisObject)
		{
			this.entities.forEach(_fn, _thisObject);
			return this;
		}
		
		function custom_forEach(_fn, _thisObject)
		{
			var things = this.entities;
			var i, elem, len = things.length;
			for (i = 0; i < len; i++)
			{
				elem = things[i];
				_fn.call(_thisObject, elem, i, things);
			}
			
			return this;
		}
	}();

	Gimme.ext.map = function()
	{
		return typeof Array.prototype.map !== 'undefined' ? native_map : custom_map;
		
		function native_map(_fn, _thisObject)
		{
			return this.entities.map(_fn, _thisObject);
		}
		
		function custom_map(_fn, _thisObject)
		{
			var result = [];
			this.forEach(function(_el, _i)
			{
				result.push(_fn.call(_thisObject, _el));
			});
			
			return result;
		}
	}();

	Gimme.ext.contains = function(_item)
	{
		return contains(this.entities, _item);
	};

	Gimme.ext.indexOf = function(_item)
	{
		return indexOf(this.entities, _item);
	};	

	
	// Helper Area: private members and functions
	//================================
	
	// private members
	var evtHash = {};
	
	// private functions
	var setOpacity = function()
	{
		function w3c_opacity(_elem, _amt) { _elem.style.opacity = _amt; }		
		function ie_opacity(_elem, _amt) { _elem.style.filter = 'alpha(opacity=' + (_amt * 100) + ')'; }
	
		var fn, d = document.createElement('div');
		if (typeof d.style.opacity !== 'undefined')
		{
			fn = w3c_opacity;
		}
		else if (typeof d.style.filter !== 'undefined')
		{
			fn = ie_opacity;
		}
		else
		{
			fn = function() {};			// No opacity support I guess :-(
		}
		
		d = null;
		return fn;	
	}();

	var getOpacity = function()
	{
		function w3c_opacity(_elem)	{ return parseFloat(_elem.style.opacity) || 'opacity';	}
		function ie_opacity(_elem)
		{
			var filter = _elem.currentStyle.filter;
			var match = filter.match(/pacity\s*=\s*(\d{1,3}.?\d*)\)/);
			if (!match)
			{
				return 1;
			}
			else
			{
				return parseFloat(match[1]) / 100;
			}
		}
		
		var fn, d = document.createElement('div');
		if (typeof d.style.opacity !== 'undefined')
		{
			fn = w3c_opacity;
		}
		else if (typeof d.style.filter !== 'undefined')
		{
			fn = ie_opacity;
		}
		else
		{
			fn = function() {};			// No opacity support I guess :-(
		}
		
		d = null;
		return fn;	
	}();
	
})();

